"""Dataset.py

This file defines the Dataset class.

TODO:
format checking...	Done!
labelings...		Done!
weights...			Done!
extensibility...	Needed?
error checking...	Done!
writing...			Done!
unit tests...		Done!
iterator...
"""
import os

from Dataset.Parser import default_parser

class Dataset(object):
	"""
	Instances of this class represent a dataset.

	Instance fields:
	self.title -- the title of the dataset (no programmatic significance)
	self.file_path -- the path of the source file of the dataset
	self.file_name -- the name of the source file of the dataset
	self.instance_count -- the number of instances in the dataset
	self.attribute_count -- the number of attributes in each instance
	self.label_count -- the number of labels in each instance
	self.column_metadata_string -- the string defining each column's
		attribute/label status
	self.data -- the data of the dataset, in the following format:
		(attributes, labels, weights)
	"""

	@property
	def title(self):
		"""The title of the dataset (no programmatic significance)"""
		return self._title
	
	@property
	def file_path(self):
		"""The path of the source file of the dataset."""
		return self._file_path

	@property
	def file_name(self):
		"""The name of the source file of the dataset."""
		return self._file_name

	@property
	def instance_count(self):
		"""The number of instances in the dataset."""
		return self._instance_count

	@property
	def attribute_count(self):
		"""The number of attributes in each instance"""
		return self._attribute_count

	@property
	def label_count(self):
		"""The number of labels in each instance."""
		return self._label_count

	@property
	def column_metadata_string(self):
		"""The string defining each column's attribute/label status."""
		return self._column_metadata_string

	@property
	def data(self):
		"""The data of the dataset, in the following format:
		(attributes, labels, weights)"""
		return self._data

	def __init__(self, title, file_path, parser=default_parser):
		"""
		Reads in a file and extracts data.
		
		Arguments:
		title -- the title of the dataset (no programmatic significance)
		file_name -- the path to the file to read in
		parser -- the function responsible for constructing a tuple representing a
		record of the data
		"""
		# Read all data
		data = []
		file_handle = open(file_path)
		for line in file_handle:
			instance = parser(line)
			data.append(instance)

		# Ensure the data is rectangular
		columns = len(data[0][0])
		for i,(attributes, labels, weight) in enumerate(data):
			if len(attributes) != columns:
				raise ValueError(r"""The data is not rectangular.
Row {0} has {1} columns, instead of {2}.""".format(i, len(attributes), columns))

		# Record metadata
		self._title = title
		self._file_path = file_path
		self._file_name = os.path.basename(file_path)
		self._data = data
		self._column_metadata_string = 'a' * len(data[0][0])
		self._instance_count = len(data)
		self._attribute_count = len(data[0][0])
		self._label_count = len(data[0][1])
		self._labeled = False

	def label(self, column_metadata_string):
		"""
		Partitions each record into attributes and labels.

		Arguments:
		metadata -- a tuple with indicies indicating each  the column is an
		attribute or a label. 'a' characters indicate that a column represents
		an attribute; 'l' characters indicate that a column represents a label.

		Example:
		data:
		[
			( (1,2,3,4,5), (), 1 ),
			( (1,2,3,4,5), (), 1 ),
			( (1,2,3,4,5), (), 1 ),
			...
		]
		
		metadata_string: "aaall"
		
		labeled data:
		[
			( (1,2,3), (4,5), 1 ),
			( (1,2,3), (4,5), 1 ),
			( (1,2,3), (4,5), 1 ),
			...
		]
		"""
		# Ensure that column_metadata_string is not malformed
		if not set(column_metadata_string) == {'a', 'l'} or \
			not len(column_metadata_string) == len(self._column_metadata_string):
			raise ValueError("Malformed column_metadata_string")

		# Ensure that data is unlabeled
		if self._labeled:
			self.unlabel()

		new_data = []
		# Since the data is unlabeled and unweighted, we only worry about
		# attributes
		for attributes,labels,weight in self._data:
			new_attributes = []
			new_labels = []
			for index,column in enumerate(column_metadata_string):
				if column == 'a':
					new_attributes.append(attributes[index])
				elif column == 'l':
					new_labels.append(attributes[index])
			new_data.append( (new_attributes, new_labels, 1) )
		
		# Record metadata
		self._data = new_data
		self._column_metadata_string = column_metadata_string
		self._labeled = True
		self._attribute_count = len(self._data[0][0])
		self._label_count = len(self._data[0][1])

	
	def unlabel(self):
		"""
		Reconstructs the original dataset, before seperating columns into
		attributes and labels.
		
		Example:
		labeled data:
		[
			( (1,2,4), (3,5), 1 ),
			( (1,2,4), (3,5), 1 ),
			( (1,2,4), (3,5), 1 ),
			...
		]
		column_metadata_string: "aalal"
		
		after unlabel():
		[
			( (1,2,3,4,5), (), 1 ),
			( (1,2,3,4,5), (), 1 ),
			( (1,2,3,4,5), (), 1 ),
			...
		]
		
		"""
		new_data = []
		for attributes,labels,weight in self._data:
			new_attributes = []
			attribute_index = 0
			label_index = 0
			for column in self.column_metadata_string:
				if column == 'a':
					new_attributes.append(attributes[attribute_index])
					attribute_index += 1
				elif column == 'l':
					new_attributes.append(labels[label_index])
					label_index += 1
			new_attributes = tuple(new_attributes)
			new_data.append( (new_attributes, (), weight) )
		
		# Record metadata
		self._data = new_data
		self._column_metadata_string = 'a' * len(self.data[0][0])
		self._labeled = False
		self._attribute_count = len(self._data[0][0])
		self._label_count = len(self._data[0][1])

	def __str__(self):
		return r"""Dataset
Title:				{0}
Source File:		{1}
Instance Count:		{2}
Attribute Count:	{3}
Label Count:		{4}
""".format(
			self._title,
			self._file_name,
			self._instance_count,
			self._attribute_count,
			self._label_count)

	def write(self, file_path):
		"""
		Writes the dataset to a file.

		Arguments:
		file_path -- the path of the file to write the dataset to
		"""
		file_handle = open(file_path,"w")
		for attributes,labels,weight in self.data:
			temp_string = ", ".join(attributes + labels) + "\n"
			file_handle.write(temp_string)